Explora poderosas alternativas a los enums en TypeScript: const assertions y tipos de unión. Aprende cuándo usar cada uno para un código robusto y mantenible.
Más Allá de los Enums: Const Assertions vs. Tipos de Unión en TypeScript
En el mundo de JavaScript con tipado estático con TypeScript, los enums han sido durante mucho tiempo una opción predilecta para representar un conjunto fijo de constantes con nombre. Ofrecen una forma clara y legible de definir una colección de valores relacionados. Sin embargo, a medida que los proyectos crecen y evolucionan, los desarrolladores a menudo buscan alternativas más flexibles y, a veces, más eficientes. Dos potentes contendientes que surgen con frecuencia son las aserciones const (const assertions) y los tipos de unión (union types). Este artículo profundiza en los matices del uso de estas alternativas a los enums tradicionales, proporcionando ejemplos prácticos y guiándote sobre cuándo elegir cada una.
Entendiendo los Enums Tradicionales de TypeScript
Antes de explorar las alternativas, es esencial tener una comprensión sólida de cómo funcionan los enums estándar de TypeScript. Los enums te permiten definir un conjunto de constantes numéricas o de cadena con nombre. Pueden ser numéricos (el valor por defecto) o basados en cadenas.
Enums Numéricos
Por defecto, a los miembros de un enum se les asignan valores numéricos comenzando desde 0.
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Salida: 0
También puedes asignar explícitamente valores numéricos.
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Salida: 200
Enums de Cadena (String)
Los enums de cadena suelen ser preferidos por su mejor experiencia de depuración, ya que los nombres de los miembros se conservan en el JavaScript compilado.
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Salida: "BLUE"
La Sobrecarga de los Enums
Aunque los enums son convenientes, conllevan una ligera sobrecarga. Cuando se compilan a JavaScript, los enums de TypeScript se convierten en objetos que a menudo tienen mapeos inversos (por ejemplo, mapear el valor numérico de vuelta al nombre del enum). Esto puede ser útil, pero también contribuye al tamaño del paquete (bundle) y podría no ser siempre necesario.
Considera este simple enum de cadena:
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
En JavaScript, esto podría convertirse en algo como:
var Status;
(function (Status) {
Status["Pending"] = "PENDING";
Status["Processing"] = "PROCESSING";
Status["Completed"] = "COMPLETED";
})(Status || (Status = {}));
Para conjuntos simples de constantes de solo lectura, este código generado puede parecer un poco excesivo.
Alternativa 1: Const Assertions (Aserciones Const)
Las aserciones const son una característica poderosa de TypeScript que te permite decirle al compilador que infiera el tipo más específico posible para un valor. Cuando se usan con arrays u objetos destinados a representar un conjunto fijo de valores, pueden servir como una alternativa ligera a los enums.
Const Assertions con Arrays
Puedes crear un array de literales de cadena y luego usar una aserción const para hacer que su tipo sea inmutable y sus elementos sean tipos literales.
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Error: El tipo '"FAILED"' no es asignable al tipo 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Procesando estado: ${status}`);
}
processStatus("COMPLETED");
Analicemos lo que está sucediendo aquí:
as const: Esta aserción le dice a TypeScript que trate el array como de solo lectura e infiera los tipos literales más específicos para sus elementos. Así, en lugar de `string[]`, el tipo se convierte en `readonly ["PENDING", "PROCESSING", "COMPLETED"]`.typeof statusArray[number]: Este es un tipo mapeado. Itera sobre todos los índices destatusArrayy extrae sus tipos literales. La firma de índicenumberesencialmente dice "dame el tipo de cualquier elemento en este array". El resultado es un tipo de unión:"PENDING" | "PROCESSING" | "COMPLETED".
Este enfoque proporciona una seguridad de tipos similar a los enums de cadena, pero genera un JavaScript mínimo. El statusArray en sí mismo sigue siendo un array de cadenas en JavaScript.
Const Assertions con Objetos
Las aserciones const son aún más potentes cuando se aplican a objetos. Puedes definir un objeto donde las claves representan tus constantes con nombre y los valores son las cadenas o números literales.
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Error: El tipo '"GUEST"' no es asignable al tipo 'UserRole'.
function displayRole(role: UserRole) {
console.log(`El rol del usuario es: ${role}`);
}
displayRole(userRoles.Admin); // Válido
displayRole("EDITOR"); // Válido
En este ejemplo de objeto:
as const: Esta aserción hace que todo el objeto sea de solo lectura. Más importante aún, infiere tipos literales para todos los valores de las propiedades (por ejemplo,"ADMIN"en lugar destring) y hace que las propiedades mismas sean de solo lectura.keyof typeof userRoles: Esta expresión resulta en una unión de las claves del objetouserRoles, que es"Admin" | "Editor" | "Viewer".typeof userRoles[keyof typeof userRoles]: Este es un tipo de búsqueda (lookup type). Toma la unión de claves y la usa para buscar los valores correspondientes en el tipouserRoles. Esto resulta en la unión de los valores:"ADMIN" | "EDITOR" | "VIEWER", que es nuestro tipo deseado para los roles.
La salida de JavaScript para userRoles será un objeto JavaScript plano:
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
Esto es significativamente más ligero que un enum típico.
Cuándo Usar Const Assertions
- Constantes de solo lectura: Cuando necesitas un conjunto fijo de literales de cadena o número que no deben cambiar en tiempo de ejecución.
- Salida de JavaScript mínima: Si te preocupa el tamaño del paquete y quieres la representación en tiempo de ejecución más eficiente para tus constantes.
- Estructura similar a un objeto: Cuando prefieres la legibilidad de los pares clave-valor, similar a cómo podrías estructurar datos o configuración.
- Conjuntos basados en cadenas: Particularmente útil para representar estados, tipos o categorías que se identifican mejor con cadenas descriptivas.
Alternativa 2: Tipos de Unión (Union Types)
Los tipos de unión te permiten declarar que una variable puede contener un valor de uno de varios tipos. Cuando se combinan con tipos literales (literales de cadena, número, booleano), forman una forma poderosa de definir un conjunto de valores permitidos sin necesitar una declaración de constante explícita para el conjunto en sí.
Tipos de Unión con Literales de Cadena
Puedes definir directamente una unión de literales de cadena.
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Error: El tipo '"BLUE"' no es asignable al tipo 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Cambiando luz a: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Error
Esta es la forma más directa y a menudo la más concisa de definir un conjunto de valores de cadena permitidos.
Tipos de Unión con Literales Numéricos
De manera similar, puedes usar literales numéricos.
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Error: El tipo '201' no es asignable al tipo 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("¡Éxito!");
} else {
console.log(`Código de error: ${code}`);
}
}
handleResponse(500);
Cuándo Usar Tipos de Unión
- Conjuntos simples y directos: Cuando el conjunto de valores permitidos es pequeño, claro y no requiere claves descriptivas más allá de los valores mismos.
- Constantes implícitas: Cuando no necesitas referirte a una constante con nombre para el conjunto en sí, sino usar directamente los valores literales.
- Máxima concisión: Para escenarios sencillos donde definir un objeto o array dedicado parece excesivo.
- Parámetros de función/tipos de retorno: Excelente para definir el conjunto exacto de entradas/salidas de cadena o número aceptables para las funciones.
Comparando Enums, Const Assertions y Tipos de Unión
Resumamos las diferencias clave y los casos de uso:
Comportamiento en Tiempo de Ejecución
- Enums: Generan objetos JavaScript, potencialmente con mapeos inversos.
- Const Assertions (Arrays/Objetos): Generan arrays u objetos JavaScript planos. La información de tipo se elimina en tiempo de ejecución, pero la estructura de datos permanece.
- Tipos de Unión (con literales): No hay representación en tiempo de ejecución para la unión en sí. Los valores son solo literales. La comprobación de tipos ocurre puramente en tiempo de compilación.
Legibilidad y Expresividad
- Enums: Alta legibilidad, especialmente con nombres descriptivos. Pueden ser más verbosos.
- Const Assertions (Objetos): Buena legibilidad a través de pares clave-valor, imitando configuraciones o ajustes.
- Const Assertions (Arrays): Menos legibles para representar constantes con nombre, más para una simple lista ordenada de valores.
- Tipos de Unión: Muy concisos. La legibilidad depende de la claridad de los propios valores literales.
Seguridad de Tipos
- Los tres enfoques ofrecen una fuerte seguridad de tipos. Aseguran que solo se puedan asignar valores válidos y predefinidos a las variables o pasarlos a las funciones.
Tamaño del Paquete (Bundle)
- Enums: Generalmente los más grandes debido a los objetos JavaScript generados.
- Const Assertions: Más pequeños que los enums, ya que producen estructuras de datos planas.
- Tipos de Unión: Los más pequeños, ya que no generan ninguna estructura de datos específica en tiempo de ejecución para el tipo en sí, solo se basan en valores literales.
Matriz de Casos de Uso
Aquí hay una guía rápida:
| Característica | Enum de TypeScript | Const Assertion (Objeto) | Const Assertion (Array) | Tipo de Unión (Literales) |
|---|---|---|---|---|
| Salida en Tiempo de Ejecución | Objeto JS (con mapeo inverso) | Objeto JS plano | Array JS plano | Ninguna (solo valores literales) |
| Legibilidad (Constantes con nombre) | Alta | Alta | Media | Baja (los valores son los nombres) |
| Tamaño del Paquete | El más grande | Mediano | Mediano | El más pequeño |
| Flexibilidad | Buena | Buena | Buena | Excelente (para conjuntos simples) |
| Uso Común | Estados, Códigos de Estado, Categorías | Configuración, Definiciones de Roles, Feature Flags | Listas ordenadas de valores inmutables | Parámetros de función, valores restringidos simples |
Ejemplos Prácticos y Mejores Prácticas
Ejemplo 1: Representando Códigos de Estado de una API
Enum:
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Const Assertion (Objeto):
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Tipo de Unión:
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Recomendación: Para este escenario, un tipo de unión es a menudo la opción más concisa y eficiente. Los propios valores literales son suficientemente descriptivos. Si necesitaras asociar metadatos adicionales con cada estado (por ejemplo, un mensaje amigable para el usuario), un objeto con aserción const sería una mejor elección.
Ejemplo 2: Definiendo Roles de Usuario
Enum:
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... lógica ...
}
Const Assertion (Objeto):
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... lógica ...
}
Tipo de Unión:
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... lógica ...
}
Recomendación: Un objeto con aserción const logra un buen equilibrio aquí. Proporciona pares clave-valor claros (por ejemplo, userRolesObject.Admin) que pueden mejorar la legibilidad al hacer referencia a los roles, sin dejar de ser eficiente. Un tipo de unión también es un contendiente muy fuerte si los literales de cadena directos son suficientes.
Ejemplo 3: Representando Opciones de Configuración
Imagina un objeto de configuración para una aplicación global que podría tener diferentes temas.
Enum:
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... otras opciones de configuración ...
}
Const Assertion (Objeto):
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... otras opciones de configuración ...
}
Tipo de Unión:
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... otras opciones de configuración ...
}
Recomendación: Para ajustes de configuración como los temas, el objeto con aserción const es a menudo ideal. Define claramente las opciones disponibles y sus valores de cadena correspondientes. Las claves (Light, Dark, System) son descriptivas y se mapean directamente a los valores, haciendo que el código de configuración sea muy comprensible.
Eligiendo la Herramienta Adecuada para el Trabajo
La decisión entre enums de TypeScript, aserciones const y tipos de unión no siempre es blanco o negro. A menudo se reduce a un equilibrio entre el rendimiento en tiempo de ejecución, el tamaño del paquete y la legibilidad/expresividad del código.
- Opta por Tipos de Unión cuando necesites un conjunto simple y restringido de literales de cadena o número y se desee la máxima concisión. Son excelentes para firmas de funciones y restricciones de valor básicas.
- Opta por Const Assertions (con Objetos) cuando quieras una forma más estructurada y legible de definir constantes con nombre, similar a un enum, pero con una sobrecarga en tiempo de ejecución significativamente menor. Esto es genial para configuraciones, roles o cualquier conjunto donde las claves añaden un significado importante.
- Opta por Const Assertions (con Arrays) cuando simplemente necesites una lista ordenada inmutable de valores, y el acceso directo a través del índice sea más importante que las claves con nombre.
- Considera los Enums de TypeScript cuando necesites sus características específicas, como el mapeo inverso (aunque esto es menos común en el desarrollo moderno) o si tu equipo tiene una fuerte preferencia y el impacto en el rendimiento es insignificante para tu proyecto.
En muchos proyectos modernos de TypeScript, encontrarás una inclinación hacia las aserciones const y los tipos de unión sobre los enums tradicionales, especialmente para constantes basadas en cadenas, debido a sus mejores características de rendimiento y a una salida de JavaScript a menudo más simple.
Consideraciones Globales
Al desarrollar aplicaciones para una audiencia global, las definiciones de constantes consistentes y predecibles son cruciales. Las opciones que hemos discutido (enums, aserciones const, tipos de unión) contribuyen a esta consistencia al reforzar la seguridad de tipos en diferentes entornos y configuraciones regionales de los desarrolladores.
- Consistencia: Independientemente del método elegido, la clave es la consistencia dentro de tu proyecto. Si decides usar objetos con aserción const para los roles, mantén ese patrón en toda la base de código.
- Internacionalización (i18n): Al definir etiquetas o mensajes que serán internacionalizados, utiliza estas estructuras con seguridad de tipos para garantizar que solo se usen claves o identificadores válidos. Las cadenas traducidas reales se gestionarán por separado a través de bibliotecas de i18n. Por ejemplo, si tienes un campo `status` que puede ser "PENDING", "PROCESSING", "COMPLETED", tu biblioteca de i18n mapearía estos identificadores internos a un texto de visualización localizado.
- Zonas Horarias y Monedas: Aunque no está directamente relacionado con los enums, recuerda que al tratar con valores como fechas, horas o monedas, el sistema de tipos de TypeScript puede ayudar a hacer cumplir el uso correcto, pero generalmente se necesitan bibliotecas externas para un manejo global preciso. Por ejemplo, se podría definir un tipo de unión `Currency` como `"USD" | "EUR" | "GBP"`, pero la lógica de conversión real requiere herramientas especializadas.
Conclusión
TypeScript proporciona un rico conjunto de herramientas para gestionar constantes. Si bien los enums nos han servido bien, las aserciones const y los tipos de unión ofrecen alternativas convincentes y, a menudo, más eficientes. Al entender sus diferencias y elegir el enfoque correcto según tus necesidades específicas —ya sea rendimiento, legibilidad o concisión— puedes escribir un código TypeScript más robusto, mantenible y eficiente que escale globalmente.
Adoptar estas alternativas puede llevar a tamaños de paquete más pequeños, aplicaciones más rápidas y una experiencia de desarrollo más predecible para tu equipo internacional.